/*
 * Copyright 2010 Francisco Modesto
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.googlecode.gwt.tinyaop.rebind;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.googlecode.gwt.tinyaop.client.InterceptableProxy;
import com.googlecode.gwt.tinyaop.client.Interceptor;
import com.googlecode.gwt.tinyaop.client.Invocation;
import com.googlecode.gwt.tinyaop.client.MethodDescriptor;

public class ClassProxyCreator extends AbstractCreator {

  public ClassProxyCreator(JClassType type, Class<? extends Annotation>[] custom) {
    super(type, custom);
  }

  @Override
  public String create(TreeLogger logger, GeneratorContext context) {
    String pkgeName = classType.getPackage().getName();
    String className = getGeneratedName();
    
    PrintWriter writer = context.tryCreate(logger, pkgeName, className);
    if (writer != null) {
      ClassSourceFileComposerFactory factory = createFactory(pkgeName, className);
      SourceWriter src = factory.createSourceWriter(context, writer);
      createBody(src);
      //src.println("CRASH"); // Is there a better way to know what is going on?
      src.commit(logger);
    }
    return pkgeName + "." + className;
  }

  private ClassSourceFileComposerFactory createFactory(String pkge, String name) {
    ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(pkge, name);
    factory.addImport(Interceptor.class.getCanonicalName());
    factory.addImport(Invocation.class.getCanonicalName());
    factory.addImport(MethodDescriptor.class.getCanonicalName());
    factory.addImport(InterceptableProxy.class.getCanonicalName());
    factory.addImport(Annotation.class.getCanonicalName());
    factory.setSuperclass(classType.getQualifiedSourceName());
    factory.addImplementedInterface(InterceptableProxy.class.getSimpleName());

    return factory;
  }

  private void createBody(SourceWriter src) {
    int index = 0;
    for (JMethod method : classType.getOverridableMethods()) {
      if (needsIntercept(method)) {
        createMethod(src, method, index++);
      }
    }
    createProxyMethods(src, index);
  }

  private boolean needsIntercept(JMethod method) {
    return findInterceptors(method).length > 0;
  }

  private void createMethod(SourceWriter src, JMethod method, int index) {
    src.println();
    new FieldBuilder(method, index, annotations).create(src);
    src.println();
    Builder invoke = interceptor(index, findInterceptors(method), 0, targetCall(method));
    Builder body = new ReturnBuilder(method.getReturnType(), invoke);
    new MethodBuilder(method, body, true).create(src);
  }

  private Builder interceptor(int method, Class<?>[] array, int index, Builder builder) {
    Builder child;
    if (index == array.length - 1) {
      child = builder;
    }
    else {
      JClassType object = classType.getOracle().getJavaLangObject();
      Builder next = interceptor(method, array, index + 1, builder);
      child = new ReturnBuilder(object, next);
    }
    return new InterceptorBuilder(method, array[index], child);
  }
  
  private Builder targetCall(JMethod method) {
    String args = MethodBuilder.arguments(method, true);
    String call =  getGeneratedName() + ".super." + method.getName() + "(" + args + ");";
    if (method.getReturnType().equals(JPrimitiveType.VOID)) {
      return new RawBuilder(call, "return null;");
    }
    else {
      return new RawBuilder("return " + call);
    }
  }

  private void createProxyMethods(SourceWriter src, int methods) {
    new ProxyBuilder(methods, false).create(src);
  }
}
